#!/usr/bin/python
import MimeWriter
import re 
import smtplib
import time
import cPickle as pickle
import os
import socket
import sys

from gui import configuration, master

from sys import argv
from optparse import OptionParser
from xml.sax import make_parser
from xml.sax.handler import ContentHandler
from email.mime.text import MIMEText



class DailyDigest:
    def __init__(self):
        self.errorMessage = ""
        self.message = ""
        
    def ddopts(self):
        options = OptionParser()
        options.add_option("-g", "--go", dest="go", action="store_true", help="reads and emails rss streams to the set accout. uses stored settings.")
        options.add_option("-v", "--verbose", dest="verbose", action="store_true", help="print extended output info")
        options.add_option("-q", "--quiet", dest="verbose", action="store_false", help="turn verbose mode off")
        options.add_option("-u", "--username", dest="username", help="[REQUIRED] sets/changes the smtp username to send reports from", metavar="USERNAME")
        options.add_option("-p", "--password", dest="password", help="[REQUIRED] sets/changes the smtp password of the account that sends reports", metavar="PASSWORD")
        options.add_option("-s", "--server", dest="server", help="[REQUIRED] your smtp server", metavar="SERVER")
        options.add_option("-t", "--to", dest="email_to", help="[REQUIRED] the target email address", metavar="EMAILADDR")
        options.add_option("-S", "--stylesheet", dest="stylesheet", help="specify the stylesheet to be used in the output email. default is ipodtouch.css.", metavar="STYLESHEET")
        options.add_option("-a", "--add", dest="add_feed", help="add a feed to the list", metavar="URL")
        options.add_option("-l", "--list", dest="list_feed", action="store_true", help="displays a list of all the rss feeds")
        options.add_option("-r", "--remove", dest="remove_feed", help="remove a feed from the list", metavar="URL")
        options.add_option("-m", "--max", dest="max_posts", help="control information overload by specicifying max posts per feed", metavar="NUM")
        (opts, args) = options.parse_args()
        return opts
    
    def send_digest(self, state):
        try:
            styleFile = state.settings['stylesheet']
            if styleFile == False or styleFile == None:
                styleFile = "ipodtouch.css"
                
            STYLESHEET = open("stylesheets/%s" % styleFile,"r")
            message = ''.join(["<style> ", STYLESHEET.read(), " </style>", 
                              "<div style=\"font-family: Times New Roman, sans-serif; ",
                              "font-size: 14px;padding: 0 10px 0 10px;\">"])
            STYLESHEET.close()
        except IOError:
            self.errorMessage = "Couldn't open %s..." % state.settings['stylesheet']
            quit()
    
        if (state.settings['max_posts'] <> None) and (state.settings['max_posts'] <> False):
            maxPosts = state.settings['max_posts']
        else:
            maxPosts = 50  # Arbitrary hardcoded value ftw!
    
        newPostsCount = 0
        for feed in state.feedManager.feedList:        
            #print time.strftime("%Y%m%d %H:%M:%S"), "-> Parsing %s" % feed.feedTitle
            
            try:
                rssReader = RssReader(feed)
                parser = make_parser()
                parser.setContentHandler(rssReader)
                parser.parse(feed.url)
                                    
                newPosts = ""
                feedPostsCount = 0
                for post in feed.posts:
                    if feed.sentDates.count(post['date']) == 0 and int(feedPostsCount)< int(maxPosts):                    
                        #print "New post found: ", post['title']
                        newPostsCount = newPostsCount + 1
                        feedPostsCount = feedPostsCount + 1
                        item=''.join(["<div class=post>",
                                        "<div class=postHeader>", 
                                          "<div class=postTitle>",  "<a class=title href=\"", post['link'], "\">", post['title'], "</a>", "</div>", 
                                          post['date'], 
                                         "</div>",
                                         "<div class=postContent>",
                                             post['description'], post['content'], 
                                         "</div>",
                                      "</div>" ])
                        
                        newPosts = ''.join([newPosts,item])
                        feed.sentDates.append(post['date'])
        
                if newPosts <> "":
                    streamTitle = "<div class=feedTitle>%s</div>" % feed.feedTitle 
                    streamDescription = "<div class=feedDescription>%s</div>" % feed.feedDescription
                    message = ''.join([message, streamTitle, streamDescription, newPosts])
            except socket.error:
                self.errorMessage = "A socket error occured."
    
        if newPostsCount > 0:
            emailAgent = EmailAgent(state.settings['username'], state.settings['password'], state.settings['server'])
            emailAgent.add_recipient(state.settings['email_to'])
            emailAgent.sendmail("Daily updates for %s" % time.strftime("%m-%d-%Y"), message, state.settings['verbose'])    
            
            if emailAgent.errorMessage <> "": 
                self.errorMessage = emailAgent.errorMessage
            else:
                self.message = "Sent %s new posts..." % newPostsCount
                state.save_feeds() # save the dates of the posts sent...



class StateMachine:
    """
    StateMachine stores settings & feeds. It uses python's cPickle to save itself.
    All calls to configuration info, feed info, etc should be routed through here. 
    """
    def __init__(self, username=False, password=False, server=False, verbose="", email_to=False, stylesheet=False, max_posts = False):
        self.load_state(username, password, server, verbose, email_to, stylesheet, max_posts)
        self.load_feeds()
        #self.state_to_console()
        self.save_settings()
    
    def state_to_console(self):
        # force required settings to exist in state file
        try:
            print "username --> %s" % self.settings['username'] 
            print "password --> %s" % self.settings['password']
            print "server --> %s" % self.settings['server']
            print "email_to --> %s" % self.settings['email_to']
        except KeyError:
            print "FATAL ERROR: There was an error reading required settings." 
            quit()
        
        otherSettings = ['verbose', 'stylesheet', 'max_posts', 'send_on_load']
        for s in otherSettings:
            try:
                print "%s" % s,
                print " --> %s" % self.settings[s]
            except KeyError:
                self.settings[s] = False
                print "%s" % s,
                print " --> %s" % self.settings[s]        

    def open_os_filename(self, filename, filemode):
        """ Route calls to open files through here, does some ops based on OS. """
        if os.name == "mac" or os.name == "posix":
            FILE = open(".%s" % filename, filemode)
        elif os.name == "nt":
            data_path = ''.join(["C:\\Documents and Settings\\",
                               os.getenv("Username"),
                               "\\Application Data\\daily-digest\\"])
            fn = ''.join([data_path, filename])
            if os.access(data_path, os.W_OK ):
                FILE = open("%s.dat" % fn, filemode)
            else:
                if filemode == "r":
                    raise IOError
                else:
                    os.mkdir(data_path)
                    FILE = open("%s.dat" % filename, filemode)
        else:
            FILE = open("%s" % filename, filemode)
        return FILE
    
    def load_feeds(self):
        """ Called by __init__ to restore feeds from pickle jar. """
        try:
            FEED_FILE = self.open_os_filename("feedlist", "r")
            self.feedManager = pickle.load(FEED_FILE)
            FEED_FILE.close()
        except IOError:
            self.feedManager = FeedManager()

    def load_state(self, username=False, password=False, server=False, verbose=False, email_to=False, stylesheet=False, max_posts = False):
        """ Use to load/change settings. Called by init. Restores settings from pickle jar. """
        try:
            SETTINGS_FILE = self.open_os_filename("state", "r")
            self.settings = pickle.load(SETTINGS_FILE)
            SETTINGS_FILE.close()
        except IOError:
            self.settings = {
                        'username': username,
                        'password': password,
                        'server': server,
                        'verbose': verbose,
                        'email_to': email_to,
                        'stylesheet': stylesheet,
                        'max_posts': max_posts,
                        'send_on_load': False
                        }

        if (username <> False) and (username <> None): self.settings['username'] = username
        if (password <> False) and (password <> None): self.settings['password'] = password
        if (server <> False) and (server <> None): self.settings['server'] = server
        if (verbose <> "") and (verbose <> None): self.settings['verbose'] = verbose
        if (email_to <> False) and (email_to <> None): self.settings['email_to'] = email_to
        if (stylesheet <> False) and (stylesheet <> None): self.settings['stylesheet'] = stylesheet
        if (max_posts <> False) and (max_posts <> None): self.settings['max_posts'] = max_posts
        
        
    def save_feeds(self):
        """ saves FeedManager instance to a pickled file named 'feedlist' """
        self.feedManager.clean_up()
        FEED_FILE = self.open_os_filename("feedlist", "w")
        pickle.dump(self.feedManager, FEED_FILE)
        FEED_FILE.close()

    def save_settings(self):
        """ saves self.settings to a pickled file named 'state' """
        SETTINGS_FILE = self.open_os_filename("state", "w")            
        pickle.dump(self.settings, SETTINGS_FILE)
        SETTINGS_FILE.close()
        
class FeedManager:
    """ Manages the list of RSS feeds. """
    def __init__(self):
        self.feedList = []
    
    def clean_up(self):
        """ Clear posts before pickling FeedManager. """
        for feed in self.feedList:
            feed.titles = []
            feed.links = []
            feed.descriptions = []
            feed.dates = []
            feed.content = []
            feed.posts = []
    
    def new_feed(self, url):
        """ 
        Tries to add a new feed to the FeedManager.
        Does a primary parse of the URL to validate and get Title/Description. 
        Sets errorMessage if there's a problem
        """
        self.errorMessage = None
        try:
            newFeed = RssFeed(url)
            rssReader = RssReader(newFeed)
            parser = make_parser()
            parser.setContentHandler(rssReader)
            parser.parse(url)
            
            addFeed = True
            for feed in self.feedList:
                if feed.url == newFeed.url:
                    addFeed = False
                    
            if addFeed:
                self.feedList.append(newFeed)
            else:
                self.errorMessage =  "this feed is already in the list."
                
        except IOError:
            self.errorMessage = "couldn't parse. not a valid feed"

    def feeds_to_console(self):
        """
        Prints a numerically annotated list of feeds.
        """
        for i in xrange(0, len(self.feedList)):
            print "  %s :" % i, self.feedList[i].feedTitle, 
            if self.feedList[i].feedDescription <> "":
                print " - ", self.feedList[i].feedDescription
            else: print ""
        print ""

    def remove_feed(self, index):
        """
        Remove a feed from FeedManager. Index is the location in the list.
        """
        try:
            removed_feed = self.feedList.pop(int(index))
            self.message = "removed feed: %s" % removed_feed.feedTitle
        except IndexError:
            self.errorMessage = "couldn't remove feed #%s" % index
        
class RssFeed:
    """ Stores parsed RSS information """
    def __init__(self, url):
        self.url = url
        self.titles = []
        self.links = []
        self.descriptions = []
        self.dates = []
        self.items = 0
        self.feedTitle = ""
        self.feedDescription = ""
        self.content = []
        self.storedTitle, self.storedDescription = False, False
        self.sentDates = []
        self.posts = []
    
    def add_sentDate(self, date):
        self.sentDates.append(date)
            
    def add_title(self, title):
        self.titles.append(title)
    
    def add_link(self, url):
        self.links.append(url)
    
    def add_desc(self, desc):
        self.descriptions.append(desc)
    
    def add_content(self, content):
        self.content.append(content)
    
    def add_date(self, date):
        self.dates.append(date)

    def set_feed_title(self, title):
        if self.storedTitle == False: 
            self.feedTitle = title
            self.storedTitle = True
        
    def set_feed_description(self, description):
        if self.storedDescription == False:
            self.feedDescription = description
            self.storedDescription = True
    
    def add_post(self, post):
        self.posts.append(post)
        
class RssRegex:
    """ Helper class for XML parsing, inherited by RssReader """
    def compile_regex(self):
        self.titleRegex = re.compile("[Tt]itle")
        self.linkRegex = re.compile("[Ll]ink")
        self.descRegex = re.compile("[Dd]escription")
        self.contentRegex = re.compile("[Cc]ontent")
        self.dateRegex = re.compile("[Dd]ate")
        self.channelRegex = re.compile("[Cc]hannel")
    
    def isTitle(self, name):
        if re.search(self.titleRegex, name) <> None: 
            return True
    
    def isLink(self, name):
        if re.search(self.linkRegex, name) <> None: 
            return True

    def isDescription(self, name):
        if re.search(self.descRegex, name) <> None: 
            return True
    
    def isContent(self, name):
        if re.search(self.contentRegex, name) <> None:
            return True
        
    def isDate(self, name):
        if re.search(self.dateRegex, name) <> None: 
            return True
    
    def isChannel(self, name):
        if re.search(self.channelRegex, name) <> None:
            return True


class RssReader(ContentHandler, RssRegex):
    """ Deal with RSS streams """    
    def __init__(self, rssFeed):
        self.inTitle, self.inLink, self.inItem = False, False, False 
        self.inDate, self.inDescription, self.inChannel = False, False, False
        self.inContent = False
        self.rssFeed = rssFeed
        self.compile_regex()
    
    def startElement(self, name, attrs):
        if name == "item" or name == "entry": 
            self.inItem = True
            self.rssFeed.items = self.rssFeed.items + 1

            self._content = ""
            self._title = ""
            self._link = ""
            self._description = ""
            self._date = ""

            self.newPost = {
                            'title': None,
                            'link': None,
                            'description': None,
                            'content': None,
                            'date': None
                            }
            
        elif self.isContent(name):
            self.inContent = True
        elif self.isTitle(name): 
            self._title = ""
            self.inTitle = True
        elif self.isLink(name): 
            self._link = ""
            self.inLink = True
        elif self.isDescription(name):
            self._description = ""
            self.inDescription = True
        elif self.isDate(name):
            self._date = ""
            self.inDate = True
        elif self.isChannel(name):
            self.inChannel = True
    
    def characters (self, ch):
        if self.inTitle: self._title += ch
        if self.inLink: self._link += ch
        if self.inDescription: self._description += ch
        if self.inDate: self._date += ch
        if self.inContent: self._content += ch
            
    def endElement(self, name):
        # TODO:
        # get the doc encoding type instead of just hardcoding utf8
        if name == "item" or name == "entry":
            self.inItem = False
            self.newPost['title'] = self._title.encode('utf8')
            self.newPost['description'] = self._description.encode('utf8')
            self.newPost['content'] = self._content.encode('utf8')
            self.newPost['date'] = self._date.encode('utf8')
            self.newPost['link'] = self._link.encode('utf8')
            self.rssFeed.add_post(self.newPost)
            
        if self.isChannel(name):
            self.inChannel = False
        if self.inItem:
            if self.isTitle(name):
                self.rssFeed.add_title(self._title.encode('utf8'))
                self.inTitle = False
            if self.isLink(name): 
                self.rssFeed.add_link(self._link.encode('utf8'))
                self.inLink = False
            if self.isDescription(name):
                self.rssFeed.add_desc(self._description.encode('utf8'))
                self.inDescription = False
            if self.isContent(name):
                self.rssFeed.add_content(self._content.encode('utf8'))
                self.inContent = False
            if self.isDate(name):
                self.rssFeed.add_date(self._date.encode('utf8'))
                self.inDate = False
        else:
            if self.isTitle(name):
                self.rssFeed.set_feed_title(self._title.encode('utf8'))
            if self.isDescription(name):
                self.rssFeed.set_feed_description(self._description.encode('utf8'))


class EmailAgent:
    def __init__(self, username, password, smtpserver):
        self.username = username
        self.smtpserver = smtpserver
        self.recipients = []
        self.server = smtplib.SMTP(self.smtpserver)

        try:
            if self.username <> "": self.server.login(self.username, password)    
        except smtplib.SMTPException:
            self.errorMessage = "SMTP AUTH extension not supported by this server"
        
    def add_recipient(self, to):
        self.recipients.append(to)
        
    def sendmail(self, subject, message, verbose=False):
        msg = MIMEText(message)
        msg.set_type('text/html')
        msg['Subject'] = subject
        msg['From'] = "null@nowhere.com"
        
        try:   
            if verbose: self.server.set_debuglevel(1)
            self.server.sendmail(self.username, self.recipients, msg.as_string())
        except smtplib.SMTPAuthenticationError:
            self.errorMessage = "Couldn't authenticate smtp account."
        except smtplib.SMTPSenderRefused:
            self.errorMessage = "The SMTP server refused the connection."
        except smtplib.SMTPRecipientsRefused:
            self.errorMessage = "The SMTP server refused the recipient address."

if __name__ == "__main__":
    app = DailyDigest()
    opts = app.ddopts()
    state = StateMachine(opts.username, opts.password, opts.server, opts.verbose, opts.email_to, opts.stylesheet, opts.max_posts)

    # COMMAND LINE ARGS:
    if opts.add_feed <> None:
        print "looking for a feed at %s..." % opts.add_feed
        state.feedManager.new_feed(opts.add_feed)
        if state.feedManager.errorMessage:
            print state.feedManager.errorMessage
        state.save_feeds()
    
    if opts.remove_feed <> None:
        state.feedManager.remove_feed(opts.remove_feed)
        state.save_feeds()
        
    if opts.list_feed:
        state.feedManager.feeds_to_console()
    
    if opts.go:
        app.send_digest(state)        
    else:
        print "(to send the digest, run with -g option.)"

    if len(argv) == 1:
        masterWindow = master.MasterWindow(state, app)